"""
Organize Photos by Date - Professional Photo Library Management Tool
=====================================================================
File      : 03-organize_photos_by_date-v01.py
Version   : 1.0.0
Author    : Yahya Nazer
Copyright : (c) 2025 Chatbizdb.com - Yahya Nazer
License   : Proprietary
Email     : contact@chatbizdb.com
Status    : Production
Date      : 2026-05-13

Objective
---------
Ask the user to select a folder containing photos, read the shoot date from
EXIF metadata (falling back to file modified date when EXIF is absent), create
date-based subfolders, and copy or move every photo into its corresponding
date folder -- all inside a timestamped output folder created automatically
inside C-Reports/ beside the source folder.

Folder Layout
--------------
    Selected input folder  :  TOP/Photos/
    TOP folder (parent)    :  TOP/
    C-Reports root         :  TOP/C-Reports/
    Output folder          :  TOP/C-Reports/Sorted-Photos-YYYY-MM-DD--HH-MM/

    Inside the output folder, date-based subfolders are created according to
    the folder_style setting (see Date Folder Styles below):

    YYYY-MM-DD (default):
        TOP/C-Reports/Sorted-Photos-2026-05-13--14-30/
            2024-06-15/  (all photos taken on 15 June 2024)
            2024-06-16/
            2025-01-01/

    YYYY-MM:
        TOP/C-Reports/Sorted-Photos-.../
            2024-06/
            2025-01/

    YYYY:
        TOP/C-Reports/Sorted-Photos-.../
            2024/
            2025/

    YYYY/MM/DD (nested):
        TOP/C-Reports/Sorted-Photos-.../
            2024/
                06/
                    15/
                    16/
            2025/
                01/

EXIF Date Priority and Fallback
---------------------------------
    This script reads the photo date using a two-step strategy:

    Step 1 -- EXIF Date Taken (preferred):
        img.getexif() returns a dict-like object mapping numeric tag IDs to values.
        The script iterates over all tags and looks for one of three date fields:
            DateTimeOriginal   (tag 36867) -- time shutter was pressed.
            DateTimeDigitized  (tag 36868) -- time of digital conversion.
            DateTime           (tag 306)   -- last modification time (less reliable).
        EXIF date strings are formatted as "YYYY:MM:DD HH:MM:SS" (colons between
        year, month, day -- NOT slashes or dashes as in ISO 8601).
        datetime.strptime(value, "%Y:%m:%d %H:%M:%S") parses this into a datetime.

    Step 2 -- File Modified Date (fallback):
        os.path.getmtime(file_path) returns a Unix timestamp (seconds since
        1 January 1970 00:00:00 UTC) as a float.
        datetime.fromtimestamp(ts) converts it to local time as a datetime object.
        Used for files with no EXIF data (PNG, BMP, older JPEG without metadata).

EXIF Tag Lookup (TAGS dict)
-----------------------------
    PIL.ExifTags.TAGS is a dict mapping numeric tag IDs to string names:
        {306: 'DateTime', 36867: 'DateTimeOriginal', 36868: 'DateTimeDigitized', ...}

    TAGS.get(tag_id, tag_id)
        Returns the string name for the tag if it is in the lookup table.
        Returns the raw integer tag_id if the tag is not recognised.
        This prevents KeyError for proprietary or unknown camera tags.

    img.getexif() (public API) vs img._getexif() (private)
        getexif() is the current Pillow public API (v6.0+), returning an Exif
        object that also provides IFD access.  _getexif() is a legacy private
        method that returns a plain dict.  getexif() is preferred for
        forward compatibility.

shutil.copy2() vs shutil.move()
---------------------------------
    shutil.copy2(src, dst)
        Copies file content AND preserves all timestamps (mtime, atime, ctime).
        The source file remains in its original location.
        Use for non-destructive organisation where originals are kept intact.

    shutil.move(src, dst)
        Moves the file to the new location (rename if on the same filesystem,
        copy-then-delete if crossing filesystem boundaries).
        The source file is removed after the move succeeds.
        Use when you want to restructure the library without duplicating files.

    Both preserve file metadata; copy2 is always the safer first choice.

Duplicate Filename Handling
-----------------------------
    get_unique_destination_path() prevents silent overwrites:
        1. Check if destination_folder/filename already exists.
        2. If not: return that path directly.
        3. If yes: append a zero-padded counter to the base name and retry:
               photo.jpg  ->  photo_001.jpg  ->  photo_002.jpg  ...
           counter:03d formats the counter as exactly 3 digits with leading zeros.

    This covers the case where two photos from different source subfolders
    share the same filename but belong to the same date folder.

Date Folder Styles
-------------------
    Style           Example output folder       Use case
    -----------     -------------------------   -------------------------
    YYYY-MM-DD      2024-06-15                  Day-level precision (default)
    YYYY-MM         2024-06                     Month grouping
    YYYY            2024                        Year grouping (large collections)
    YYYY/MM/DD      2024/06/15  (nested)        Hierarchical explorer view

DEBUG_FLAG
-----------
    DEBUG_FLAG = True  (module level)
    debug_print(message) prints only when DEBUG_FLAG is True.
    Set to False for silent operation.

Supported Photo Extensions
----------------------------
    Standard : .jpg .jpeg .png .tif .tiff .heic
    RAW      : .cr2 .cr3 .nef .arw .raf .dng .3fr .fff

Dependencies
-------------
Required:
    pip install Pillow

Standard library (no install):
    os, sys, shutil, datetime, tkinter

Function Summary
-----------------
    Function                    Purpose
    --------------------------  --------------------------------------------------
    debug_print()               Conditional print gated by DEBUG_FLAG.
    check_dependencies()        Verify Pillow and tkinter are importable.
    select_folder()             GUI folder-picker; return selected path string.
    ask_copy_or_move()          GUI Yes/No dialog; return 'copy' or 'move'.
    create_output_folder()      Create C-Reports/ and Sorted-Photos-* subfolder.
    get_exif_date_taken()       Read DateTimeOriginal/Digitized/DateTime from EXIF.
    get_file_modified_date()    Read file modification timestamp from the OS.
    get_photo_date()            Try EXIF first; fall back to file modified date.
    create_date_folder()        Create and return the date-named subfolder path.
    get_unique_destination_path() Return a conflict-free destination path.
    is_photo_file()             Return True if the file extension is supported.
    organize_photos()           Walk input tree; copy/move each photo by date.
    main()                      Orchestrate all steps end-to-end.

Step-by-Step Flow (inside main())
------------------------------------
Step 1  : Print startup banner.
Step 2  : Dependency check (Pillow + tkinter).
Step 3  : GUI folder-picker: select the input photo folder.
Step 4  : Auto-create output folder: C-Reports/Sorted-Photos-YYYY-MM-DD--HH-MM/.
Step 5  : GUI Yes/No dialog: copy or move files.
Step 6  : Walk input folder recursively; copy/move each photo into its date folder.
Step 7  : Print and show the completion summary.

Notes
-----
- The output folder is created automatically beside the input folder inside
  C-Reports/; no second folder-picker dialog is shown.
- folder_style is set to 'YYYY-MM-DD' by default.  Change the constant at
  the top of main() or add a GUI selector in a future version.
- Photos without any readable date (corrupted EXIF AND missing mtime) are
  placed in an 'Unknown-Date' subfolder rather than being skipped entirely.
- macOS resource-fork files (names starting with '._') are skipped by
  is_photo_file() so they never appear in the output.
"""

__version__    = '1.0.0'
__author__     = 'Yahya Nazer'
__copyright__  = 'Copyright (c) 2025 Chatbizdb.com - Yahya Nazer'
__license__    = 'Proprietary'
__maintainer__ = 'Yahya Nazer'
__email__      = 'contact@chatbizdb.com'
__status__     = 'Production'

# ===========================================================================
# Imports
# ===========================================================================
import sys

# UTF-8 stdout / stderr configuration
# Reconfigure early so all subsequent print() calls handle non-ASCII filenames.
try:
    sys.stdin.reconfigure(encoding='utf-8')
    sys.stdout.reconfigure(encoding='utf-8')
    sys.stderr.reconfigure(encoding='utf-8')
except Exception:
    pass    # Python 3.6 or non-standard interpreter -- continue with defaults

import os                           # File system operations and path handling
import shutil                       # High-level file copy and move operations
from datetime import datetime       # Date parsing and timestamp formatting

import tkinter as tk                # GUI toolkit for folder picker and dialogs
from tkinter import filedialog, messagebox  # Folder dialog, Yes/No, info dialogs

# Pillow: image opening and EXIF tag name lookup
from PIL import Image
from PIL.ExifTags import TAGS       # {tag_id: 'TagName'} lookup dictionary

# ===========================================================================
# DEBUG_FLAG -- controls verbose debug output
#
# True  : debug_print() calls produce console output (verbose tracing).
# False : debug_print() calls are silently discarded (production mode).
# ===========================================================================
DEBUG_FLAG = True

# ===========================================================================
# PHOTO_EXTENSIONS -- supported file extensions (lowercase, dot-prefixed)
# ===========================================================================
PHOTO_EXTENSIONS = {
    # Standard compressed / lossless
    '.jpg', '.jpeg', '.png', '.tif', '.tiff', '.heic',
    # RAW camera formats
    '.cr2', '.cr3',     # Canon
    '.nef',             # Nikon
    '.arw',             # Sony
    '.raf',             # Fujifilm
    '.dng',             # Adobe Digital Negative
    '.3fr',             # Hasselblad
    '.fff',             # Hasselblad
}

# ===========================================================================
# UNKNOWN_DATE_FOLDER -- name for photos with no readable date
# ===========================================================================
UNKNOWN_DATE_FOLDER = 'Unknown-Date'


# ===========================================================================
# Function: debug_print()
# ===========================================================================
def debug_print(message: str) -> None:
    """
    Print a debug message only when the module-level DEBUG_FLAG is True.

    Acts as a zero-cost production gate: when DEBUG_FLAG is False the
    function body is a no-op so every debug call costs only a boolean test.

    Args:
        message (str): Message to print when debug mode is active.
    """
    if DEBUG_FLAG:
        print(message)


# ===========================================================================
# Function: check_dependencies()
# ===========================================================================
def check_dependencies() -> bool:
    """
    Verify that Pillow and tkinter are importable before the script runs.

    Checks two categories:
        Pillow   : Image.open() for EXIF reading; PIL.ExifTags.TAGS for tag names.
        tkinter  : Folder picker and GUI dialogs.

    Returns:
        bool: True if both packages are available; False otherwise.
    """
    all_ok = True

    print('[INFO]  Checking required imports...')

    for pkg, label, install in [
        ('PIL',     'Pillow (PIL)',  'pip install Pillow'),
        ('tkinter', 'tkinter',       'included with Python -- check your installation'),
    ]:
        try:
            __import__(pkg)
            print(f'[OK]  {label}')
        except ImportError:
            print(f'[X]   {label} is missing.  Fix: {install}')
            all_ok = False

    # Verify the specific Pillow submodules used in this script
    if all_ok:
        for submod, symbol in [
            ('PIL.Image',       'Image'),
            ('PIL.ExifTags',    'TAGS'),
        ]:
            try:
                mod = __import__(submod, fromlist=[symbol])
                getattr(mod, symbol)
                print(f'[OK]  {submod}.{symbol}')
            except (ImportError, AttributeError):
                print(f'[X]   {submod}.{symbol} not found -- '
                      f'run: pip install --upgrade Pillow')
                all_ok = False

    if all_ok:
        print('[INFO]  All imports verified. Ready to run.')
    else:
        print('[ERROR] Missing dependencies.  '
              'Install with:  pip install Pillow')

    return all_ok


# ===========================================================================
# Function: select_folder()
# ===========================================================================
def select_folder(title: str) -> str:
    """
    Open a GUI folder-picker dialog and return the selected folder path.

    Uses tkinter's askdirectory() which opens the OS-native folder browser.
    Returns an empty string '' if the user cancels -- the caller checks for
    a falsy return value.

    root.withdraw() hides the blank tkinter root window so only the folder
    browser dialog is visible.  root.destroy() releases all tkinter resources
    after the dialog closes.

    Args:
        title (str): Dialog window title shown to the user.

    Returns:
        str: Absolute path of the selected folder, or '' if cancelled.
    """
    root = tk.Tk()
    root.withdraw()     # Hide blank root window immediately

    folder_path = filedialog.askdirectory(title=title)

    root.destroy()      # Release tkinter resources

    return folder_path


# ===========================================================================
# Function: ask_copy_or_move()
# ===========================================================================
def ask_copy_or_move() -> str:
    """
    Show a GUI Yes/No dialog and return 'copy' or 'move'.

    messagebox.askyesno() returns True if the user clicks Yes, False for No.
    The dialog is modal -- execution blocks here until the user clicks.

    Copy vs Move:
        'copy'  : shutil.copy2() -- originals remain in place; output is
                  a new organised duplicate.  Safe; recommended for first-run.
        'move'  : shutil.move() -- originals are relocated into the output
                  folder structure.  Non-destructive to the file content but
                  removes files from their source locations.

    Returns:
        str: 'copy' if user clicks Yes, 'move' if user clicks No.
    """
    root = tk.Tk()
    root.withdraw()

    answer = messagebox.askyesno(
        'Copy or Move',
        'Do you want to COPY files?\n\n'
        'Yes = Copy files (originals stay in place)\n'
        'No  = Move files (originals are relocated)'
    )

    root.destroy()

    return 'copy' if answer else 'move'


# ===========================================================================
# Function: create_output_folder()
# ===========================================================================
def create_output_folder(input_folder: str, timestamp: str) -> str:
    """
    Create the timestamped Sorted-Photos output folder inside C-Reports/.

    Output structure:
        parent_dir  = parent of input_folder
        C-Reports/  = sibling of input_folder (created if missing)
        Sorted-Photos-<timestamp>/  = output inside C-Reports/

    Example:
        input_folder = /Photos/Vacation
        output       = /Photos/C-Reports/Sorted-Photos-2026-05-13--14-30/

    The output folder is created automatically -- no second folder-picker
    dialog is shown.  This keeps the workflow to a single folder selection.

    os.makedirs(path, exist_ok=True)
        Creates all missing parent directories in one call.
        exist_ok=True silently does nothing if the folder already exists,
        making re-runs safe within the same minute.

    Args:
        input_folder (str): Absolute path of the selected input folder.
        timestamp    (str): 'YYYY-MM-DD--HH-MM' string for naming.

    Returns:
        str: Full path of the created output folder.

    Raises:
        RuntimeError: If the output folder cannot be created.
    """
    parent_dir = os.path.dirname(input_folder)

    # C-Reports sits as a sibling of the input folder (not inside it)
    c_reports_dir = os.path.join(parent_dir, 'C-Reports')
    os.makedirs(c_reports_dir, exist_ok=True)

    # Sorted-Photos output subfolder inside C-Reports
    output_folder = os.path.join(c_reports_dir, f'Sorted-Photos-{timestamp}')
    os.makedirs(output_folder, exist_ok=True)

    print(f'[INFO]  C-Reports folder : {c_reports_dir}')
    print(f'[INFO]  Output folder    : {output_folder}')

    return output_folder


# ===========================================================================
# Function: get_exif_date_taken()
# ===========================================================================
def get_exif_date_taken(file_path: str) -> datetime | None:
    """
    Attempt to read a shoot date from the image's EXIF metadata.

    img.getexif()
        Public Pillow API (v6.0+) that returns an Exif object -- a dict-like
        container mapping numeric tag IDs (int) to their raw values.
        Returns an empty Exif object (falsy) for images with no EXIF block.

    TAGS.get(tag_id, tag_id)
        Looks up the human-readable name for a numeric tag ID.
        Returns the raw integer tag_id if the ID is not in the lookup table,
        preventing KeyError for proprietary or unknown camera tags.

    Tag priority order:
        DateTimeOriginal  (ID 36867) -- moment the shutter was pressed.
                                        Most reliable shoot date.
        DateTimeDigitized (ID 36868) -- time of digital conversion (scanner).
                                        Second choice for scanned photos.
        DateTime          (ID 306)   -- last image modification time.
                                        Least reliable; changes on edit/rotate.
        The loop returns on the FIRST match so priority is determined by the
        order the tags appear in the EXIF block (camera-dependent).

    EXIF date format:
        All three fields store dates as "YYYY:MM:DD HH:MM:SS".
        Note the COLON separators between year, month, and day -- NOT slashes
        or dashes as in ISO 8601.  The format string "%Y:%m:%d %H:%M:%S"
        matches this exactly.

    Args:
        file_path (str): Absolute path to the image file.

    Returns:
        datetime | None: Parsed shoot datetime, or None if not found.
    """
    try:
        with Image.open(file_path) as img:
            exif_data = img.getexif()   # Returns Exif object (dict-like)

            if not exif_data:
                debug_print(f'[DEBUG] No EXIF data in: '
                            f'{os.path.basename(file_path)}')
                return None

            for tag_id, value in exif_data.items():
                # TAGS.get() converts numeric ID to readable name
                tag_name = TAGS.get(tag_id, tag_id)

                if tag_name in ('DateTimeOriginal',
                                'DateTimeDigitized',
                                'DateTime'):
                    try:
                        # EXIF format: "YYYY:MM:DD HH:MM:SS"  (colons, not dashes)
                        return datetime.strptime(str(value), '%Y:%m:%d %H:%M:%S')
                    except ValueError as e:
                        debug_print(f'[DEBUG] Unparseable EXIF date '
                                    f'"{value}": {e}')
                        return None

    except Exception as e:
        debug_print(f'[DEBUG] EXIF read failed for '
                    f'{os.path.basename(file_path)}: {e}')

    return None


# ===========================================================================
# Function: get_file_modified_date()
# ===========================================================================
def get_file_modified_date(file_path: str) -> datetime:
    """
    Read the file modification timestamp from the operating system.

    os.path.getmtime(file_path)
        Returns the file's last modification time as a Unix timestamp:
        a float representing the number of seconds elapsed since the
        Unix epoch (1 January 1970 00:00:00 UTC).

    datetime.fromtimestamp(ts)
        Converts a Unix timestamp to a datetime object in local time.
        Uses the system's current timezone offset.

    Limitation:
        The file modified date changes when:
            - The file is edited or re-saved.
            - The file is copied without metadata preservation (shutil.copy,
              not shutil.copy2).
            - The file is transferred between systems.
        It is a reliable fallback only when no EXIF data exists.

    Args:
        file_path (str): Absolute path to the file.

    Returns:
        datetime: File modification datetime in local time.
    """
    modified_timestamp = os.path.getmtime(file_path)   # Unix float timestamp
    return datetime.fromtimestamp(modified_timestamp)   # Local datetime


# ===========================================================================
# Function: get_photo_date()
# ===========================================================================
def get_photo_date(file_path: str) -> tuple:
    """
    Return the best available date for a photo and the source of that date.

    Strategy:
        1. Try get_exif_date_taken() -- EXIF DateTimeOriginal / Digitized / DateTime.
        2. If EXIF returns None, fall back to get_file_modified_date().
        3. If even mtime fails (unexpected OS error), return a fixed
           'Unknown' sentinel and the 'Error' source label so the caller
           can place the file in the UNKNOWN_DATE_FOLDER.

    The date_source label ('EXIF' / 'Modified Date') is returned alongside
    the datetime so the debug log clearly shows which method was used for
    each photo -- useful when auditing the sort results.

    Args:
        file_path (str): Absolute path to the photo file.

    Returns:
        tuple: (photo_date, date_source)
            photo_date  (datetime | None): Best available date, or None on error.
            date_source (str): 'EXIF', 'Modified Date', or 'Error'.
    """
    exif_date = get_exif_date_taken(file_path)

    if exif_date:
        debug_print(f'[DEBUG] Date source: EXIF  ->  '
                    f'{exif_date.strftime("%Y-%m-%d")}  |  '
                    f'{os.path.basename(file_path)}')
        return exif_date, 'EXIF'

    try:
        mod_date = get_file_modified_date(file_path)
        debug_print(f'[DEBUG] Date source: Modified Date  ->  '
                    f'{mod_date.strftime("%Y-%m-%d")}  |  '
                    f'{os.path.basename(file_path)}')
        return mod_date, 'Modified Date'

    except Exception as e:
        debug_print(f'[DEBUG] Could not read modified date for '
                    f'{os.path.basename(file_path)}: {e}')
        return None, 'Error'


# ===========================================================================
# Function: create_date_folder()
# ===========================================================================
def create_date_folder(output_folder: str,
                        photo_date: datetime | None,
                        folder_style: str = 'YYYY-MM-DD') -> str:
    """
    Create and return the date-named destination subfolder.

    The folder name is derived from photo_date using one of four styles:

        folder_style    strftime format     Example
        -----------     ---------------     -------
        YYYY            %Y                  2024
        YYYY-MM         %Y-%m               2024-06
        YYYY-MM-DD      %Y-%m-%d            2024-06-15  (default)
        YYYY/MM/DD      nested subfolders   2024/06/15

    For YYYY/MM/DD three nested folders are created:
        output_folder/2024/06/15/
    The intermediate folders (2024/ and 2024/06/) are created by
    os.makedirs(exist_ok=True), which creates the full path in one call.

    None date (photos with unreadable date AND unreadable mtime):
        Placed in UNKNOWN_DATE_FOLDER ('Unknown-Date') so no file is lost.

    Unknown folder_style:
        Falls back to YYYY-MM-DD to ensure output is always produced.

    os.makedirs(path, exist_ok=True)
        Creates the folder and all missing parents.
        exist_ok=True: no error if the folder already exists.

    Args:
        output_folder (str):           Root output folder path.
        photo_date    (datetime|None): Photo shoot or modified datetime.
        folder_style  (str):           Date folder format key.

    Returns:
        str: Full path of the created (or existing) date subfolder.
    """
    # Handle photos with no readable date
    if photo_date is None:
        destination_folder = os.path.join(output_folder, UNKNOWN_DATE_FOLDER)
        os.makedirs(destination_folder, exist_ok=True)
        return destination_folder

    if folder_style == 'YYYY':
        folder_name        = photo_date.strftime('%Y')
        destination_folder = os.path.join(output_folder, folder_name)

    elif folder_style == 'YYYY-MM':
        folder_name        = photo_date.strftime('%Y-%m')
        destination_folder = os.path.join(output_folder, folder_name)

    elif folder_style == 'YYYY/MM/DD':
        # Three-level nested folder structure: year/month/day
        # os.makedirs handles all three levels in one call
        destination_folder = os.path.join(
            output_folder,
            photo_date.strftime('%Y'),
            photo_date.strftime('%m'),
            photo_date.strftime('%d'),
        )

    else:
        # Default: YYYY-MM-DD  (also catches any unrecognised style string)
        folder_name        = photo_date.strftime('%Y-%m-%d')
        destination_folder = os.path.join(output_folder, folder_name)

    os.makedirs(destination_folder, exist_ok=True)
    return destination_folder


# ===========================================================================
# Function: get_unique_destination_path()
# ===========================================================================
def get_unique_destination_path(destination_folder: str,
                                 file_name: str) -> str:
    """
    Return a conflict-free file path in destination_folder.

    Prevents silent overwrites when two photos from different source
    subfolders share the same filename but both sort into the same date folder.

    Algorithm:
        1. Try: destination_folder/photo.jpg
           If it does not exist: return it immediately.
        2. If it exists: append a zero-padded 3-digit counter:
               photo.jpg  ->  photo_001.jpg
               photo.jpg  ->  photo_002.jpg  (if _001 also exists)
           counter:03d formats the integer as exactly 3 digits with leading
           zeros so the files sort in numerical order in a file explorer.
        3. Increment counter and retry until a free name is found.
           (Practically this terminates within 1-2 iterations unless the
            folder already contains thousands of photos with the same name.)

    Args:
        destination_folder (str): Folder where the file will be written.
        file_name          (str): Original filename (with extension).

    Returns:
        str: Full path that does not yet exist in destination_folder.
    """
    destination_path = os.path.join(destination_folder, file_name)

    # Fast path: no conflict -- return immediately
    if not os.path.exists(destination_path):
        return destination_path

    # Conflict exists: generate a numbered variant
    base_name, extension = os.path.splitext(file_name)
    counter              = 1

    while True:
        # counter:03d  ->  001, 002, 003, ...  (three digits, leading zeros)
        new_name         = f'{base_name}_{counter:03d}{extension}'
        destination_path = os.path.join(destination_folder, new_name)

        if not os.path.exists(destination_path):
            debug_print(f'[DEBUG] Conflict resolved: {file_name} -> {new_name}')
            return destination_path

        counter += 1


# ===========================================================================
# Function: is_photo_file()
# ===========================================================================
def is_photo_file(file_name: str) -> bool:
    """
    Return True if the filename has a supported photo extension.

    Two checks are applied before the extension test:
        file_name.startswith('._')
            Skips macOS resource-fork hidden files (e.g. '._photo.jpg').
            These are metadata-only files created when photos are copied to
            non-HFS+ volumes.  They are invisible in Finder but visible to
            os.walk() and would cause Pillow errors if opened.
        os.path.splitext(file_name)[1].lower()
            Extracts the extension and lowercases it so '.JPG' == '.jpg'.

    Args:
        file_name (str): Filename (without directory path).

    Returns:
        bool: True if the file should be processed; False if it should be skipped.
    """
    # Skip macOS resource-fork files
    if file_name.startswith('._'):
        return False

    extension = os.path.splitext(file_name)[1].lower()
    return extension in PHOTO_EXTENSIONS


# ===========================================================================
# Function: organize_photos()
# ===========================================================================
def organize_photos(input_folder: str,
                     output_folder: str,
                     action_type: str,
                     folder_style: str) -> dict:
    """
    Walk the input folder recursively and copy/move each photo by date.

    os.walk(input_folder)
        Yields (root_folder, subfolders, files) for every directory in the
        tree.  Processing is recursive -- images in nested albums are included.
        root_folder changes on every iteration to reflect the current directory.

    Action type:
        'copy'  : shutil.copy2(src, dst) -- copies content AND timestamps.
        'move'  : shutil.move(src, dst) -- relocates the file.
        Both receive the unique destination path from get_unique_destination_path()
        so neither will ever overwrite an existing file.

    Error handling:
        Each file is processed inside its own try/except so a single bad file
        (corrupted JPEG, permission error) does not abort the entire run.
        Errors are counted and printed but processing continues.

    Summary dict returned:
        total_files     : Every file encountered by os.walk (photo or not).
        processed_files : Files successfully copied or moved.
        skipped_files   : Non-photo files (wrong extension, resource-fork).
        error_files     : Files that raised an exception during processing.

    Args:
        input_folder  (str): Absolute path of the source photo folder.
        output_folder (str): Absolute path of the Sorted-Photos-* output folder.
        action_type   (str): 'copy' or 'move'.
        folder_style  (str): Date subfolder format (e.g. 'YYYY-MM-DD').

    Returns:
        dict: Processing summary with keys
              'total_files', 'processed_files', 'skipped_files', 'error_files'.
    """
    total_files     = 0
    processed_files = 0
    skipped_files   = 0
    error_files     = 0

    debug_print('[INFO]  ======================================')
    debug_print('[INFO]  Starting photo organization')
    debug_print('[INFO]  ======================================')
    debug_print(f'[INFO]  Input folder  : {input_folder}')
    debug_print(f'[INFO]  Output folder : {output_folder}')
    debug_print(f'[INFO]  Action type   : {action_type}')
    debug_print(f'[INFO]  Folder style  : {folder_style}')

    for root_folder, subfolders, files in os.walk(input_folder):

        debug_print(f'[DEBUG] Scanning: {root_folder}')

        for file_name in files:
            total_files += 1

            # Skip non-photo files and macOS resource-fork files
            if not is_photo_file(file_name):
                skipped_files += 1
                debug_print(f'[DEBUG] Skipped (not a photo): {file_name}')
                continue

            source_path = os.path.join(root_folder, file_name)

            try:
                # Determine the best date and its source
                photo_date, date_source = get_photo_date(source_path)

                # Create (or reuse) the date-named destination subfolder
                destination_folder = create_date_folder(
                    output_folder = output_folder,
                    photo_date    = photo_date,
                    folder_style  = folder_style,
                )

                # Get a conflict-free destination path
                destination_path = get_unique_destination_path(
                    destination_folder = destination_folder,
                    file_name          = file_name,
                )

                # Copy or move the file
                if action_type == 'copy':
                    shutil.copy2(source_path, destination_path)
                else:
                    shutil.move(source_path, destination_path)

                processed_files += 1

                # Log each file with its date and the source of that date
                date_str = (photo_date.strftime('%Y-%m-%d')
                            if photo_date else 'Unknown')
                print(
                    f'[INFO]  [{processed_files:04d}]  '
                    f'{action_type.upper():<4}  |  '
                    f'{file_name:<40}  |  '
                    f'{date_str}  |  {date_source}'
                )

            except Exception as e:
                error_files += 1
                print(f'[ERROR] Failed: {source_path}')
                print(f'[ERROR] Reason: {e}')

    return {
        'total_files'    : total_files,
        'processed_files': processed_files,
        'skipped_files'  : skipped_files,
        'error_files'    : error_files,
    }


# ===========================================================================
# Function: main()
# ===========================================================================
def main() -> None:
    """
    Orchestrate the full photo organisation workflow end-to-end.

    Steps:
        1. Print startup banner.
        2. Dependency check (Pillow + tkinter).
        3. GUI folder-picker: select the input photo folder.
        4. Auto-create C-Reports/Sorted-Photos-YYYY-MM-DD--HH-MM/ beside input.
        5. GUI Yes/No dialog: copy or move files.
        6. Walk input folder recursively; copy/move each photo into its date folder.
        7. Print and show the completion summary.

    No second folder-picker is shown -- the output path is derived
    automatically from the input folder and the run timestamp.
    """
    print('[INFO]  ======================================')
    print('[INFO]  Organize Photos by Date  v' + __version__)
    print('[INFO]  ======================================')

    # Step 2 -- Dependency check
    if not check_dependencies():
        print('[ERROR] Missing dependencies.  Install:  pip install Pillow')
        return
    print()

    # Step 3 -- Input folder selection
    print('[STEP 3] Please select the folder containing your photos...')
    input_folder = select_folder('Select the folder containing photos')

    if not input_folder:
        print('[WARN]  No input folder selected. Exiting.')
        root = tk.Tk()
        root.withdraw()
        messagebox.showwarning('No Folder Selected',
                               'No input folder was selected. Exiting.')
        root.destroy()
        return

    print(f'[INFO]  Input folder: {input_folder}')

    # Step 4 -- Create output folder automatically inside C-Reports/
    print()
    print('[STEP 4] Creating output folder...')
    timestamp     = datetime.now().strftime('%Y-%m-%d--%H-%M')
    output_folder = create_output_folder(input_folder, timestamp)

    # Step 5 -- Ask copy or move
    print()
    print('[STEP 5] Copy or move?')
    action_type = ask_copy_or_move()
    print(f'[INFO]  Action: {action_type.upper()}')

    # folder_style controls the date subfolder naming convention
    # Options: 'YYYY-MM-DD' | 'YYYY-MM' | 'YYYY' | 'YYYY/MM/DD'
    folder_style = 'YYYY-MM-DD'
    print(f'[INFO]  Folder style: {folder_style}')

    # Step 6 -- Walk and organise
    print()
    print('[STEP 6] Organising photos...')
    summary = organize_photos(
        input_folder  = input_folder,
        output_folder = output_folder,
        action_type   = action_type,
        folder_style  = folder_style,
    )

    # Step 7 -- Completion summary
    print()
    print('[INFO]  ======================================')
    print('[DONE]  Photo organisation complete!')
    print('[INFO]  ======================================')
    print(f'[INFO]  Total files scanned   : {summary["total_files"]}')
    print(f'[INFO]  Photos processed      : {summary["processed_files"]}')
    print(f'[INFO]  Files skipped         : {summary["skipped_files"]}')
    print(f'[INFO]  Errors                : {summary["error_files"]}')
    print(f'[INFO]  Output folder         : {output_folder}')

    message = (
        'Photo organisation complete!\n\n'
        f'Total files scanned   : {summary["total_files"]}\n'
        f'Photos processed      : {summary["processed_files"]}\n'
        f'Files skipped         : {summary["skipped_files"]}\n'
        f'Errors                : {summary["error_files"]}\n\n'
        f'Output folder:\n{output_folder}'
    )

    root = tk.Tk()
    root.withdraw()
    messagebox.showinfo('Done', message)
    root.destroy()


# ===========================================================================
# Entry-point guard
# ===========================================================================
if __name__ == '__main__':
    main()
